﻿using System;
using ProxyViewModel.Attributes;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Collections.ObjectModel;
using System.Threading;

namespace ProxyViewModel {
	public class ValidationManager {
		private static readonly object _lock1 = new object();
		private static readonly object _lock2 = new object();
		private static readonly object _lock3 = new object();
		private static readonly object _lock4 = new object();
		private static readonly IDictionary<string, IDictionary<string, IList<ValidatorBaseAttribute>>> _viewModelTypesAndValidators = new
			Dictionary<string, IDictionary<string, IList<ValidatorBaseAttribute>>>();
		private static readonly Dictionary<string, PropertyInfo> _validationPropertiesMap = new Dictionary<string, PropertyInfo>();
		private static readonly Dictionary<string, WeakReference> _validationMessagesPropertiesAndCollectionsMap = new Dictionary<string, WeakReference>();
		private static readonly Dictionary<PropertyInfo, string> _propertisFriendlyNamesMap = new Dictionary<PropertyInfo, string>();

		public static void StoreValidator(Type type, string propertyName, ValidatorBaseAttribute validator) {
			Monitor.Enter(_lock1);
			try {
				IDictionary<string, IList<ValidatorBaseAttribute>> validators;
				if (_viewModelTypesAndValidators.ContainsKey(type.FullName)) {
					validators = _viewModelTypesAndValidators[type.FullName];
				}
				else {
					validators = new Dictionary<string, IList<ValidatorBaseAttribute>>();
					_viewModelTypesAndValidators[type.FullName] = validators;
				}

				if (!validators.ContainsKey(propertyName)) {
					validators[propertyName] = new List<ValidatorBaseAttribute>();
				}

				if (!validators[propertyName].Contains(validator)) {
					validators[propertyName].Add(validator);
					validator.PropertyName = propertyName;
					validator.TypeName = type.FullName;
				}
			}
			finally {
				Monitor.Exit(_lock1);
			}
		}

		public static bool IsValid(Type type, string propertyName, object toValidate, object viewModel) {
			ValidationResult[] results = Validate(type, propertyName, toValidate);

			if (results.Where(e => !e.IsValid).Count() > 0) {
				SetValidationMessages(type, propertyName, viewModel, results);
				SetIsValid(type, propertyName, viewModel, false);
				SetLastValidationMessage(type, propertyName, viewModel, results.Where(e => !e.IsValid).Last().ValidationError);
			}
			else {
				SetLastValidationMessage(type, propertyName, viewModel, string.Empty);
				SetIsValid(type, propertyName, viewModel, true);
				ClearValidationMessages(type, propertyName, viewModel);
			}
			if (viewModel is IValidatableViewModel) {
				SetViewModelValidationProperties(type, propertyName, (IValidatableViewModel)viewModel, results);
			}
			return results.Where(e => !e.IsValid).Count() > 0;
		}

		private static void SetLastValidationMessage(Type type, string propertyName, object viewModel, string errorMessage) {
			SetPropertyValue(type, string.Format("{0}LastValidationMessage", propertyName), viewModel, errorMessage);
		}

		private static void SetIsValid(Type type, string propertyName, object viewModel, bool isValid) {
			SetPropertyValue(type, string.Format("IsValid{0}", propertyName), viewModel, isValid);
		}

		private static void SetPropertyValue(Type type, string propertyName, object viewModel, object value) {
			GetPropertyInfo(type, propertyName).SetValue(viewModel, value, null);
		}

		private static PropertyInfo GetPropertyInfo(Type type, string propertyName) {
			PropertyInfo propertyInfo = null;
			string propertyCacheKey = string.Format("{0}.{1}", type.Name, propertyName);
			Monitor.Enter(_lock2);
			try {
				if (_validationPropertiesMap.ContainsKey(propertyCacheKey)) {
					propertyInfo = _validationPropertiesMap[propertyCacheKey];
				}
				else {
					propertyInfo = type.GetProperty(propertyName);
					_validationPropertiesMap.Add(propertyCacheKey, propertyInfo);
				}
			}
			finally {
				Monitor.Exit(_lock2);
			}
			return propertyInfo;
		}

		public static ValidationResult[] Validate(Type type, string propertyName, object toValidate) {
			ValidatorBaseAttribute[] validators = null;
			Monitor.Enter(_lock1);
			try {
				var viewmodelValidators = _viewModelTypesAndValidators[type.FullName];
				if (viewmodelValidators != null) {
					if (viewmodelValidators.ContainsKey(propertyName)) {
						validators = viewmodelValidators[propertyName].ToArray();
					}
				}
			}
			finally {
				Monitor.Exit(_lock1);
			}

			if (validators == null) {
				return new ValidationResult[] { };
			}

			IList<ValidationResult> validationResults = new List<ValidationResult>();
			foreach (ValidatorBaseAttribute validator in validators) {
				try {
					validationResults.Add(validator.Validate(toValidate));
				}
				catch (Exception ex) {
					throw new Exception(string.Format("Validator '{0} - {1}' threw an exception '{2}'", validator.TypeName,
						validator.GetType().FullName, ex.ToString()), ex);
				}
			}

			return validationResults.ToArray();
		}

		private static void SetViewModelValidationProperties(Type type, string propertyName, IValidatableViewModel viewModel, ValidationResult[] validationResults) {
			PropertyInfo info = GetPropertyInfo(type, propertyName);
			string propertyFreindlyName = info.Name;
			Monitor.Enter(_lock4);
			try {
				if (_propertisFriendlyNamesMap.ContainsKey(info)) {
					propertyFreindlyName = _propertisFriendlyNamesMap[info];
				}
				else {
					ValidationFriendlyNameAttribute freindlyNameAttribute =
						info.GetCustomAttributes(typeof(ValidationFriendlyNameAttribute), true).OfType<ValidationFriendlyNameAttribute>().FirstOrDefault();

					if (freindlyNameAttribute != null) {
						propertyFreindlyName = freindlyNameAttribute.Name;
					}
					_propertisFriendlyNamesMap.Add(info, propertyFreindlyName);
				}
			}
			finally {
				Monitor.Exit(_lock4);
			}

			ValidationMessage[] messages = viewModel.ValidationMessages.Where(e => e.PropertyName == propertyFreindlyName).ToArray();
			messages.ToList().ForEach(e => viewModel.ValidationMessages.Remove(e));
			validationResults.Where(e => !e.IsValid).ToList().ForEach(e => viewModel.ValidationMessages.Add(new ValidationMessage { PropertyName = propertyFreindlyName, Message = e.ValidationError }));
			viewModel.IsViewModelValid = viewModel.ValidationMessages.Count == 0;
		}

		private static void ClearValidationMessages(Type type, string propertyName, object viewModel) {
			ObservableCollection<string> errors = RetrieveValidationMessagesProperty(type, propertyName, viewModel);
			errors.Clear();
		}

		private static void SetValidationMessages(Type type, string propertyName, object viewModel, ValidationResult[] validationResults) {
			ObservableCollection<string> errors = RetrieveValidationMessagesProperty(type, propertyName, viewModel);
			errors.Clear();
			validationResults.Where(e => !e.IsValid).ToList().ForEach(e => errors.Add(e.ValidationError));
		}

		private static ObservableCollection<string> RetrieveValidationMessagesProperty(Type type, string propertyName, object viewModel) {
			Monitor.Enter(_lock3);
			try {
				string propertyNameKey = string.Format("{0}.{1}.{2}", type.FullName, propertyName, viewModel.GetHashCode());
				if (_validationMessagesPropertiesAndCollectionsMap.ContainsKey(propertyNameKey)) {
					WeakReference collectionReference = _validationMessagesPropertiesAndCollectionsMap[propertyNameKey];
					if (collectionReference.IsAlive || collectionReference.Target != null) {
						return (ObservableCollection<string>)collectionReference.Target;
					}
					else {
						_validationMessagesPropertiesAndCollectionsMap.Remove(propertyNameKey);
					}
				}
				ObservableCollection<string> errorsCollection =
					(ObservableCollection<string>)type.GetProperty(string.Format("{0}ValidationErrors", propertyName)).GetValue(viewModel, null);
				_validationMessagesPropertiesAndCollectionsMap[propertyNameKey] = new WeakReference(errorsCollection);
				return errorsCollection;
			}
			finally {
				Monitor.Exit(_lock3);
			}
		}
	}
}
